{-# LANGUAGE LambdaCase #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2025 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Galley.API.Teams
  ( createBindingTeam,
    createNonBindingTeamH,
    updateTeamH,
    updateTeamStatus,
    getTeamH,
    getTeamInternalH,
    getTeamNameInternalH,
    getBindingTeamMembers,
    getManyTeams,
    deleteTeam,
    addTeamMember,
    getTeamConversationRoles,
    getTeamMembers,
    bulkGetTeamMembers,
    getTeamMember,
    deleteTeamMember,
    deleteNonBindingTeamMember,
    updateTeamMember,
    getTeamConversations,
    getTeamConversation,
    deleteTeamConversation,
    getSearchVisibility,
    setSearchVisibility,
    getSearchVisibilityInternal,
    setSearchVisibilityInternal,
    uncheckedAddTeamMember,
    uncheckedGetTeamMember,
    uncheckedDeleteTeamMember,
    uncheckedUpdateTeamMember,
    userIsTeamOwner,
    canUserJoinTeam,
    ensureNotTooLargeForLegalHold,
    ensureNotTooLargeToActivateLegalHold,
    internalDeleteBindingTeam,
    updateTeamCollaborator,
    removeTeamCollaborator,
  )
where

import Brig.Types.Team (TeamSize (..))
import Cassandra (PageWithState (pwsResults), pwsHasMore)
import Cassandra qualified as C
import Control.Lens
import Data.ByteString.Conversion (List, toByteString)
import Data.ByteString.Conversion qualified
import Data.ByteString.Lazy qualified as LBS
import Data.Default
import Data.HashMap.Strict qualified as HM
import Data.Id
import Data.Json.Util
import Data.LegalHold qualified as LH
import Data.Map qualified as Map
import Data.Proxy
import Data.Qualified
import Data.Range as Range
import Data.Set qualified as Set
import Data.Singletons
import Data.Time.Clock (UTCTime)
import Galley.API.Action
import Galley.API.Error as Galley
import Galley.API.LegalHold.Team
import Galley.API.Teams.Features.Get
import Galley.API.Teams.Notifications qualified as APITeamQueue
import Galley.API.Update qualified as API
import Galley.API.Util
import Galley.App
import Galley.Effects
import Galley.Effects.Queue qualified as E
import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData
import Galley.Effects.TeamMemberStore qualified as E
import Galley.Env
import Galley.Options
import Galley.Types.Teams
import Imports hiding (forkIO)
import Polysemy
import Polysemy.Error
import Polysemy.Input
import Polysemy.TinyLog qualified as P
import System.Logger qualified as Log
import Wire.API.Conversation (ConvType (..), ConversationRemoveMembers (..))
import Wire.API.Conversation qualified
import Wire.API.Conversation.Role (wireConvRoles)
import Wire.API.Conversation.Role qualified as Public
import Wire.API.Error
import Wire.API.Error.Galley
import Wire.API.Event.LeaveReason
import Wire.API.Event.Team
import Wire.API.Federation.Client (FederatorClient)
import Wire.API.Federation.Error
import Wire.API.Push.V2 (RecipientClients (RecipientClientsAll))
import Wire.API.Routes.Internal.Galley.TeamsIntra
import Wire.API.Routes.MultiTablePaging (MultiTablePage (..), MultiTablePagingState (..))
import Wire.API.Routes.Public.Galley.TeamMember
import Wire.API.Team
import Wire.API.Team qualified as Public
import Wire.API.Team.Collaborator qualified as Collaborator
import Wire.API.Team.Collaborator qualified as TeamCollaborator
import Wire.API.Team.Conversation
import Wire.API.Team.Conversation qualified as Public
import Wire.API.Team.Feature
import Wire.API.Team.Member
import Wire.API.Team.Member qualified as M
import Wire.API.Team.Member qualified as Public
import Wire.API.Team.Permission (Perm (..), Permissions (..), SPerm (..), copy, fullPermissions, self)
import Wire.API.Team.Role
import Wire.API.Team.SearchVisibility
import Wire.API.Team.SearchVisibility qualified as Public
import Wire.API.User qualified as U
import Wire.BrigAPIAccess qualified as Brig
import Wire.BrigAPIAccess qualified as E
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
import Wire.ListItems qualified as E
import Wire.NotificationSubsystem
import Wire.Sem.Now
import Wire.Sem.Now qualified as Now
import Wire.Sem.Paging.Cassandra
import Wire.StoredConversation
import Wire.TeamCollaboratorsSubsystem
import Wire.TeamJournal (TeamJournal)
import Wire.TeamJournal qualified as Journal
import Wire.TeamStore qualified as E
import Wire.TeamSubsystem (TeamSubsystem)
import Wire.TeamSubsystem qualified as TeamSubsystem
import Wire.UserList

getTeamH ::
  forall r.
  (Member (ErrorS 'TeamNotFound) r, Member (Queue DeleteItem) r, Member TeamStore r, Member TeamSubsystem r) =>
  UserId ->
  TeamId ->
  Sem r Public.Team
getTeamH zusr tid =
  maybe (throwS @'TeamNotFound) pure =<< lookupTeam zusr tid

getTeamInternalH ::
  ( Member (ErrorS 'TeamNotFound) r,
    Member TeamStore r
  ) =>
  TeamId ->
  Sem r TeamData
getTeamInternalH tid =
  E.getTeam tid >>= noteS @'TeamNotFound

getTeamNameInternalH ::
  ( Member (ErrorS 'TeamNotFound) r,
    Member TeamStore r
  ) =>
  TeamId ->
  Sem r TeamName
getTeamNameInternalH tid =
  getTeamNameInternal tid >>= noteS @'TeamNotFound

getTeamNameInternal :: (Member TeamStore r) => TeamId -> Sem r (Maybe TeamName)
getTeamNameInternal = fmap (fmap TeamName) . E.getTeamName

-- | DEPRECATED.
--
-- The endpoint was designed to query non-binding teams. However, non-binding teams is a feature
-- that has never been adopted by clients, but the endpoint also returns the binding team of a user and it is
-- possible that this is being used by a client, even though unlikely.
--
-- The following functionality has been changed: query parameters will be ignored, which has the effect
-- that regardless of the parameters the response will always contain the binding team of the user if
-- it exists. Even though they are ignored, the use of query parameters will not result in an error.
--
-- (If you want to be pedantic, the `size` parameter is still honored: its allowed range is
-- between 1 and 100, and that will always be an upper bound of the result set of size 0 or
-- one.)
getManyTeams ::
  ( Member TeamStore r,
    Member (Queue DeleteItem) r,
    Member (ListItems LegacyPaging TeamId) r,
    Member TeamSubsystem r
  ) =>
  UserId ->
  Sem r Public.TeamList
getManyTeams zusr =
  withTeamIds zusr Nothing (toRange (Proxy @100)) $ \more ids -> do
    teams <- mapM (lookupTeam zusr) ids
    pure (Public.newTeamList (catMaybes teams) more)

lookupTeam ::
  ( Member TeamStore r,
    Member (Queue DeleteItem) r,
    Member TeamSubsystem r
  ) =>
  UserId ->
  TeamId ->
  Sem r (Maybe Public.Team)
lookupTeam zusr tid = do
  tm <- TeamSubsystem.internalGetTeamMember zusr tid
  if isJust tm
    then do
      t <- E.getTeam tid
      when (Just PendingDelete == (tdStatus <$> t)) $ do
        void $ E.tryPush (TeamItem tid zusr Nothing)
      pure (tdTeam <$> t)
    else pure Nothing

createNonBindingTeamH ::
  (Member (ErrorS InvalidAction) r) =>
  UserId ->
  ConnId ->
  a ->
  Sem r TeamId
createNonBindingTeamH _ _ _ = do
  -- non-binding teams are not supported anymore
  throwS @InvalidAction

createBindingTeam ::
  ( Member NotificationSubsystem r,
    Member Now r,
    Member TeamStore r
  ) =>
  TeamId ->
  UserId ->
  NewTeam ->
  Sem r TeamId
createBindingTeam tid zusr body = do
  let owner = Public.mkTeamMember zusr fullPermissions Nothing LH.defUserLegalHoldStatus
  team <-
    E.createTeam (Just tid) zusr body.newTeamName body.newTeamIcon body.newTeamIconKey Binding

  E.createTeamMember tid owner
  now <- Now.get
  let e = newEvent tid now (EdTeamCreate team)
  pushNotifications
    [ def
        { origin = Just zusr,
          json = toJSONObject e,
          recipients = [userRecipient zusr]
        }
    ]
  pure tid

updateTeamStatus ::
  ( Member BrigAPIAccess r,
    Member (ErrorS 'InvalidTeamStatusUpdate) r,
    Member (ErrorS 'TeamNotFound) r,
    Member Now r,
    Member TeamStore r,
    Member TeamJournal r
  ) =>
  TeamId ->
  TeamStatusUpdate ->
  Sem r ()
updateTeamStatus tid (TeamStatusUpdate newStatus cur) = do
  oldStatus <- fmap tdStatus $ E.getTeam tid >>= noteS @'TeamNotFound
  valid <- validateTransition (oldStatus, newStatus)
  when valid $ do
    runJournal newStatus cur
    E.setTeamStatus tid newStatus
  where
    runJournal Suspended _ = Journal.teamSuspend tid
    runJournal Active c = do
      teamCreationTime <- E.getTeamCreationTime tid
      -- When teams are created, they are activated immediately. In this situation, Brig will
      -- most likely report team size as 0 due to ES taking some time to index the team creator.
      -- This is also very difficult to test, so is not tested.
      (TeamSize possiblyStaleSize) <- E.getSize tid
      let size =
            if possiblyStaleSize == 0
              then 1
              else possiblyStaleSize
      Journal.teamActivate tid size c teamCreationTime
    runJournal _ _ = throwS @'InvalidTeamStatusUpdate
    validateTransition :: (Member (ErrorS 'InvalidTeamStatusUpdate) r) => (TeamStatus, TeamStatus) -> Sem r Bool
    validateTransition = \case
      (PendingActive, Active) -> pure True
      (Active, Active) -> pure False
      (Active, Suspended) -> pure True
      (Suspended, Active) -> pure True
      (Suspended, Suspended) -> pure False
      (_, _) -> throwS @'InvalidTeamStatusUpdate

updateTeamH ::
  ( Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS ('MissingPermission ('Just 'SetTeamData))) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member TeamStore r,
    Member TeamSubsystem r
  ) =>
  UserId ->
  ConnId ->
  TeamId ->
  Public.TeamUpdateData ->
  Sem r ()
updateTeamH zusr zcon tid updateData = do
  zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid
  void $ permissionCheckS SSetTeamData zusrMembership
  E.setTeamData tid updateData
  now <- Now.get
  admins <- E.getTeamAdmins tid
  let e = newEvent tid now (EdTeamUpdate updateData)
  let r = userRecipient zusr : map userRecipient (filter (/= zusr) admins)
  pushNotifications
    [ def
        { origin = Just zusr,
          json = toJSONObject e,
          recipients = r,
          conn = Just zcon,
          transient = True
        }
    ]

deleteTeam ::
  forall r.
  ( Member BrigAPIAccess r,
    Member (Error AuthenticationError) r,
    Member (ErrorS 'DeleteQueueFull) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS OperationDenied) r,
    Member (ErrorS 'TeamNotFound) r,
    Member (Queue DeleteItem) r,
    Member TeamStore r,
    Member TeamSubsystem r
  ) =>
  UserId ->
  ConnId ->
  TeamId ->
  Public.TeamDeleteData ->
  Sem r ()
deleteTeam zusr zcon tid body = do
  team <- E.getTeam tid >>= noteS @'TeamNotFound
  case tdStatus team of
    Deleted -> throwS @'TeamNotFound
    PendingDelete ->
      queueTeamDeletion tid zusr (Just zcon)
    _ -> do
      checkPermissions team
      queueTeamDeletion tid zusr (Just zcon)
  where
    checkPermissions team = do
      void $ permissionCheck DeleteTeam =<< TeamSubsystem.internalGetTeamMember zusr tid
      when (tdTeam team ^. teamBinding == Binding) $ do
        ensureReAuthorised zusr (body ^. tdAuthPassword) (body ^. tdVerificationCode) (Just U.DeleteTeam)

-- This can be called by stern
internalDeleteBindingTeam ::
  ( Member (ErrorS 'NoBindingTeam) r,
    Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'NotAOneMemberTeam) r,
    Member (ErrorS 'DeleteQueueFull) r,
    Member (Queue DeleteItem) r,
    Member TeamStore r,
    Member TeamSubsystem r
  ) =>
  TeamId ->
  Bool ->
  Sem r ()
internalDeleteBindingTeam tid force = do
  mbTeamData <- E.getTeam tid
  case tdTeam <$> mbTeamData of
    Nothing -> throwS @'TeamNotFound
    Just team | team ^. teamBinding /= Binding -> throwS @'NoBindingTeam
    Just team -> do
      mems <- TeamSubsystem.internalGetTeamMembersWithLimit tid (Just (unsafeRange 2))
      case mems ^. teamMembers of
        [mem] -> queueTeamDeletion tid (mem ^. userId) Nothing
        -- if the team has more than one member (and deletion is forced) or no members we use the team creator's userId for deletion events
        xs | null xs || force -> queueTeamDeletion tid (team ^. teamCreator) Nothing
        _ -> throwS @'NotAOneMemberTeam

getTeamConversationRoles ::
  ( Member (ErrorS 'NotATeamMember) r,
    Member TeamSubsystem r
  ) =>
  UserId ->
  TeamId ->
  Sem r Public.ConversationRolesList
getTeamConversationRoles zusr tid = do
  void $ TeamSubsystem.internalGetTeamMember zusr tid >>= noteS @'NotATeamMember
  -- NOTE: If/when custom roles are added, these roles should
  --       be merged with the team roles (if they exist)
  pure $ Public.ConversationRolesList wireConvRoles

getTeamMembers ::
  ( Member (ErrorS 'NotATeamMember) r,
    Member BrigAPIAccess r,
    Member (TeamMemberStore CassandraPaging) r,
    Member P.TinyLog r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  TeamId ->
  Maybe (Range 1 Public.HardTruncationLimit Int32) ->
  Maybe TeamMembersPagingState ->
  Sem r TeamMembersPage
getTeamMembers lzusr tid mbMaxResults mbPagingState = do
  let uid = tUnqualified lzusr
  member <- TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'NotATeamMember
  let mState = C.PagingState . LBS.fromStrict <$> (mbPagingState >>= mtpsState)
  let mLimit = fromMaybe (unsafeRange Public.hardTruncationLimit) mbMaxResults
  if member `hasPermission` SearchContacts
    then do
      pws :: PageWithState TeamMember <- E.listTeamMembers @CassandraPaging tid mState mLimit
      -- FUTUREWORK: Remove this via-Brig filtering when user and
      -- team_member tables are migrated to Postgres. We currently
      -- can't filter in the database because Cassandra doesn't
      -- support joins. The SQL could otherwise be (pseudocode):
      -- `select team_member.* from team_member, user where
      -- team_member.user = user.id where searchable`.
      let pwsResults0 = pwsResults pws
      users <-
        HM.fromList . map (\user -> (qUnqualified (U.userQualifiedId user), user))
          <$> E.getUsers (map (^. userId) pwsResults0)

      let results = flip mapMaybe pwsResults0 $ \tm ->
            let uid' = tm ^. userId
                mapSearchable user = Just tm <* guard (U.userSearchable user)
             in maybe (Just $ Left uid') (fmap Right . mapSearchable) $ HM.lookup uid' users
      let notFoundUserUids = lefts results
      unless (null notFoundUserUids) $
        P.err $
          Log.field "targets" (show notFoundUserUids)
            . Log.field "action" (Log.val "Teams.getTeamMembers")
            . Log.msg (Log.val "Could not find team members with target uids from Brig")
      pure $ toTeamMembersPage member $ pws {pwsResults = rights results}
    else do
      -- If the user does not have the SearchContacts permission (e.g. the external partner),
      -- we only return the person who invited them and the self user.
      let invitee = member ^. invitation <&> fst
      let uids = uid : maybeToList invitee
      TeamSubsystem.internalSelectTeamMembers tid uids <&> toTeamSingleMembersPage member
  where
    toTeamMembersPage :: TeamMember -> C.PageWithState TeamMember -> TeamMembersPage
    toTeamMembersPage member p =
      let withPerms = (member `canSeePermsOf`)
       in TeamMembersPage $
            MultiTablePage
              { mtpResults = map (setOptionalPerms withPerms) $ pwsResults p,
                mtpHasMore = pwsHasMore p,
                mtpPagingState = teamMemberPagingState p
              }

    toTeamSingleMembersPage :: TeamMember -> [TeamMember] -> TeamMembersPage
    toTeamSingleMembersPage member =
      mkSingleTeamMembersPage . map (setOptionalPerms (member `canSeePermsOf`))

-- | like 'getTeamMembers', but with an explicit list of users we are to return.
bulkGetTeamMembers ::
  ( Member (ErrorS 'BulkGetMemberLimitExceeded) r,
    Member (ErrorS 'NotATeamMember) r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  TeamId ->
  Maybe (Range 1 HardTruncationLimit Int32) ->
  U.UserIdList ->
  Sem r TeamMemberListOptPerms
bulkGetTeamMembers lzusr tid mbMaxResults uids = do
  unless (length (U.mUsers uids) <= fromIntegral (fromRange (fromMaybe (unsafeRange Public.hardTruncationLimit) mbMaxResults))) $
    throwS @'BulkGetMemberLimitExceeded
  m <- TeamSubsystem.internalGetTeamMember (tUnqualified lzusr) tid >>= noteS @'NotATeamMember
  mems <- TeamSubsystem.internalSelectTeamMembers tid (U.mUsers uids)
  let withPerms = (m `canSeePermsOf`)
      hasMore = ListComplete
  pure $ setOptionalPermsMany withPerms (newTeamMemberList mems hasMore)

getTeamMember ::
  ( Member (ErrorS 'TeamMemberNotFound) r,
    Member (ErrorS 'NotATeamMember) r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  TeamId ->
  UserId ->
  Sem r TeamMemberOptPerms
getTeamMember lzusr tid uid = do
  m <-
    TeamSubsystem.internalGetTeamMember (tUnqualified lzusr) tid
      >>= noteS @'NotATeamMember
  let withPerms = (m `canSeePermsOf`)
  member <- TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'TeamMemberNotFound
  pure $ setOptionalPerms withPerms member

uncheckedGetTeamMember ::
  ( Member (ErrorS 'TeamMemberNotFound) r,
    Member TeamSubsystem r
  ) =>
  TeamId ->
  UserId ->
  Sem r TeamMember
uncheckedGetTeamMember tid uid =
  TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'TeamMemberNotFound

addTeamMember ::
  forall r.
  ( Member BrigAPIAccess r,
    Member NotificationSubsystem r,
    Member (ErrorS 'InvalidPermissions) r,
    Member (ErrorS 'NoAddToBinding) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS 'NotConnected) r,
    Member (ErrorS OperationDenied) r,
    Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'TooManyTeamMembers) r,
    Member (ErrorS 'TooManyTeamAdmins) r,
    Member (ErrorS 'UserBindingExists) r,
    Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r,
    Member (Input Opts) r,
    Member Now r,
    Member LegalHoldStore r,
    Member TeamFeatureStore r,
    Member TeamNotificationStore r,
    Member TeamStore r,
    Member P.TinyLog r,
    Member (Input FanoutLimit) r,
    Member (Input (FeatureDefaults LegalholdConfig)) r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  TeamId ->
  Public.NewTeamMember ->
  Sem r ()
addTeamMember lzusr zcon tid nmem = do
  let zusr = tUnqualified lzusr
  let uid = nmem ^. nUserId
  P.debug $
    Log.field "targets" (toByteString uid)
      . Log.field "action" (Log.val "Teams.addTeamMember")
  -- verify permissions
  zusrMembership <-
    TeamSubsystem.internalGetTeamMember zusr tid
      >>= permissionCheck AddTeamMember
  let targetPermissions = nmem ^. nPermissions
  targetPermissions `ensureNotElevated` zusrMembership
  ensureNonBindingTeam tid
  ensureUnboundUsers [uid]
  ensureConnectedToLocals zusr [uid]
  (TeamSize sizeBeforeJoin) <- E.getSize tid
  ensureNotTooLargeForLegalHold tid (fromIntegral sizeBeforeJoin + 1)
  void $ addTeamMemberInternal tid (Just zusr) (Just zcon) nmem

-- This function is "unchecked" because there is no need to check for user binding (invite only).
uncheckedAddTeamMember ::
  forall r.
  ( Member BrigAPIAccess r,
    Member NotificationSubsystem r,
    Member (ErrorS 'TooManyTeamMembers) r,
    Member (ErrorS 'TooManyTeamAdmins) r,
    Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r,
    Member (Input Opts) r,
    Member Now r,
    Member LegalHoldStore r,
    Member P.TinyLog r,
    Member TeamFeatureStore r,
    Member TeamNotificationStore r,
    Member TeamStore r,
    Member (Input FanoutLimit) r,
    Member (Input (FeatureDefaults LegalholdConfig)) r,
    Member TeamJournal r
  ) =>
  TeamId ->
  NewTeamMember ->
  Sem r ()
uncheckedAddTeamMember tid nmem = do
  (TeamSize sizeBeforeJoin) <- E.getSize tid
  ensureNotTooLargeForLegalHold tid (fromIntegral sizeBeforeJoin + 1)
  (TeamSize sizeBeforeAdd) <- addTeamMemberInternal tid Nothing Nothing nmem
  owners <- E.getBillingTeamMembers tid
  Journal.teamUpdate tid (sizeBeforeAdd + 1) owners

uncheckedUpdateTeamMember ::
  forall r.
  ( Member BrigAPIAccess r,
    Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'TeamMemberNotFound) r,
    Member (ErrorS 'TooManyTeamAdmins) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member P.TinyLog r,
    Member TeamStore r,
    Member TeamJournal r,
    Member TeamSubsystem r
  ) =>
  Maybe (Local UserId) ->
  Maybe ConnId ->
  TeamId ->
  NewTeamMember ->
  Sem r ()
uncheckedUpdateTeamMember mlzusr mZcon tid newMem = do
  let mZusr = tUnqualified <$> mlzusr
  let targetMember = ntmNewTeamMember newMem
  let targetId = targetMember ^. userId
      targetPermissions = targetMember ^. permissions
  P.debug $
    Log.field "targets" (toByteString targetId)
      . Log.field "action" (Log.val "Teams.updateTeamMember")

  team <- fmap tdTeam $ E.getTeam tid >>= noteS @'TeamNotFound

  previousMember <-
    TeamSubsystem.internalGetTeamMember targetId tid >>= noteS @'TeamMemberNotFound

  admins <- E.getTeamAdmins tid
  let admins' = [targetId | isAdminOrOwner targetPermissions] <> filter (/= targetId) admins
  checkAdminLimit (length admins')

  -- update target in Cassandra
  E.setTeamMemberPermissions (previousMember ^. permissions) tid targetId targetPermissions

  when (team ^. teamBinding == Binding) $ do
    (TeamSize size) <- E.getSize tid
    owners <- E.getBillingTeamMembers tid
    Journal.teamUpdate tid size owners

  now <- Now.get
  let event = newEvent tid now (EdMemberUpdate targetId (Just targetPermissions))
  pushNotifications
    [ def
        { origin = mZusr,
          json = toJSONObject event,
          recipients = map userRecipient admins',
          conn = mZcon,
          transient = True
        }
    ]
  Brig.updateSearchIndex targetId

updateTeamMember ::
  forall r.
  ( Member BrigAPIAccess r,
    Member (ErrorS 'AccessDenied) r,
    Member (ErrorS 'InvalidPermissions) r,
    Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'TeamMemberNotFound) r,
    Member (ErrorS 'TooManyTeamAdmins) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS OperationDenied) r,
    Member NotificationSubsystem r,
    Member Now r,
    Member P.TinyLog r,
    Member TeamStore r,
    Member TeamJournal r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  TeamId ->
  NewTeamMember ->
  Sem r ()
updateTeamMember lzusr zcon tid newMem = do
  let zusr = tUnqualified lzusr
  let targetMember = ntmNewTeamMember newMem
  let targetId = targetMember ^. userId
      targetPermissions = targetMember ^. permissions
  P.debug $
    Log.field "targets" (toByteString targetId)
      . Log.field "action" (Log.val "Teams.updateTeamMember")

  -- get the team and verify permissions
  user <-
    TeamSubsystem.internalGetTeamMember zusr tid
      >>= permissionCheck SetMemberPermissions

  -- user may not elevate permissions
  targetPermissions `ensureNotElevated` user
  previousMember <-
    TeamSubsystem.internalGetTeamMember targetId tid >>= noteS @'TeamMemberNotFound
  when
    ( downgradesOwner previousMember targetPermissions
        && not (canDowngradeOwner user previousMember)
    )
    $ throwS @'AccessDenied

  uncheckedUpdateTeamMember (Just lzusr) (Just zcon) tid newMem
  where
    canDowngradeOwner = canDeleteMember

    downgradesOwner :: TeamMember -> Permissions -> Bool
    downgradesOwner previousMember targetPermissions =
      permissionsRole (previousMember ^. permissions) == Just RoleOwner
        && permissionsRole targetPermissions /= Just RoleOwner

deleteTeamMember ::
  ( Member BrigAPIAccess r,
    Member ConversationStore r,
    Member (Error AuthenticationError) r,
    Member (Error InvalidInput) r,
    Member (ErrorS 'AccessDenied) r,
    Member (ErrorS 'TeamMemberNotFound) r,
    Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS OperationDenied) r,
    Member (Input Opts) r,
    Member Now r,
    Member NotificationSubsystem r,
    Member ConversationSubsystem r,
    Member TeamFeatureStore r,
    Member TeamStore r,
    Member P.TinyLog r,
    Member (Input FanoutLimit) r,
    Member TeamJournal r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  TeamId ->
  UserId ->
  Public.TeamMemberDeleteData ->
  Sem r TeamMemberDeleteResult
deleteTeamMember lusr zcon tid remove body = deleteTeamMember' lusr zcon tid remove (Just body)

deleteNonBindingTeamMember ::
  ( Member BrigAPIAccess r,
    Member ConversationStore r,
    Member (Error AuthenticationError) r,
    Member (Error InvalidInput) r,
    Member (ErrorS 'AccessDenied) r,
    Member (ErrorS 'TeamMemberNotFound) r,
    Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS OperationDenied) r,
    Member (Input Opts) r,
    Member Now r,
    Member NotificationSubsystem r,
    Member ConversationSubsystem r,
    Member TeamFeatureStore r,
    Member TeamStore r,
    Member P.TinyLog r,
    Member (Input FanoutLimit) r,
    Member TeamJournal r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  TeamId ->
  UserId ->
  Sem r TeamMemberDeleteResult
deleteNonBindingTeamMember lusr zcon tid remove = deleteTeamMember' lusr zcon tid remove Nothing

-- | 'TeamMemberDeleteData' is only required for binding teams
deleteTeamMember' ::
  ( Member BrigAPIAccess r,
    Member ConversationStore r,
    Member (Error AuthenticationError) r,
    Member (Error InvalidInput) r,
    Member (ErrorS 'AccessDenied) r,
    Member (ErrorS 'TeamMemberNotFound) r,
    Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS OperationDenied) r,
    Member (Input Opts) r,
    Member Now r,
    Member NotificationSubsystem r,
    Member ConversationSubsystem r,
    Member TeamFeatureStore r,
    Member TeamStore r,
    Member P.TinyLog r,
    Member (Input FanoutLimit) r,
    Member TeamJournal r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  ConnId ->
  TeamId ->
  UserId ->
  Maybe Public.TeamMemberDeleteData ->
  Sem r TeamMemberDeleteResult
deleteTeamMember' lusr zcon tid remove mBody = do
  P.debug $
    Log.field "targets" (toByteString remove)
      . Log.field "action" (Log.val "Teams.deleteTeamMember")
  zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid
  targetMember <- TeamSubsystem.internalGetTeamMember remove tid
  void $ permissionCheck RemoveTeamMember zusrMember
  do
    dm <- noteS @'NotATeamMember zusrMember
    tm <- noteS @'TeamMemberNotFound targetMember
    unless (canDeleteMember dm tm) $ throwS @'AccessDenied
  team <- fmap tdTeam $ E.getTeam tid >>= noteS @'TeamNotFound
  if team ^. teamBinding == Binding && isJust targetMember
    then do
      body <- mBody & note (InvalidPayload "missing request body")
      ensureReAuthorised (tUnqualified lusr) (body ^. tmdAuthPassword) Nothing Nothing
      (TeamSize sizeBeforeDelete) <- E.getSize tid
      -- TeamSize is 'Natural' and subtracting from  0 is an error
      -- TeamSize could be reported as 0 if team members are added and removed very quickly,
      -- which happens in tests
      let sizeAfterDelete =
            if sizeBeforeDelete == 0
              then 0
              else sizeBeforeDelete - 1
      E.deleteUser remove
      owners <- E.getBillingTeamMembers tid
      Journal.teamUpdate tid sizeAfterDelete $ filter (/= remove) owners
      pure TeamMemberDeleteAccepted
    else do
      getFeatureForTeam @LimitedEventFanoutConfig tid
        >>= ( \case
                FeatureStatusEnabled -> do
                  admins <- E.getTeamAdmins tid
                  uncheckedDeleteTeamMember lusr (Just zcon) tid remove (Left admins)
                FeatureStatusDisabled -> do
                  mems <- getTeamMembersForFanout tid
                  uncheckedDeleteTeamMember lusr (Just zcon) tid remove (Right mems)
            )
          . (.status)
      pure TeamMemberDeleteCompleted

-- This function is "unchecked" because it does not validate that the user has the `RemoveTeamMember` permission.
uncheckedDeleteTeamMember ::
  forall r.
  ( Member ConversationStore r,
    Member NotificationSubsystem r,
    Member ConversationSubsystem r,
    Member Now r,
    Member TeamStore r
  ) =>
  Local UserId ->
  Maybe ConnId ->
  TeamId ->
  UserId ->
  Either [UserId] TeamMemberList ->
  Sem r ()
uncheckedDeleteTeamMember lusr zcon tid remove (Left admins) = do
  now <- Now.get
  pushMemberLeaveEvent now
  E.deleteTeamMember tid remove
  -- notify all conversation members not in this team.
  removeFromConvsAndPushConvLeaveEvent lusr zcon tid remove
  where
    -- notify team admins
    pushMemberLeaveEvent :: UTCTime -> Sem r ()
    pushMemberLeaveEvent now = do
      let e = newEvent tid now (EdMemberLeave remove)
      let recipients =
            userRecipient
              <$> (tUnqualified lusr : filter (/= (tUnqualified lusr)) admins)
      pushNotifications
        [ def
            { origin = Just (tUnqualified lusr),
              json = toJSONObject e,
              recipients,
              conn = zcon,
              transient = True
            }
        ]
uncheckedDeleteTeamMember lusr zcon tid remove (Right mems) = do
  now <- Now.get
  pushMemberLeaveEventToAll now
  E.deleteTeamMember tid remove
  -- notify all conversation members not in this team.
  removeFromConvsAndPushConvLeaveEvent lusr zcon tid remove
  where
    -- notify all team members. This is to maintain compatibility with clients
    -- relying on these events, but eventually they will catch up and this
    -- function, and the corresponding feature flag, will be ready for removal.
    pushMemberLeaveEventToAll :: UTCTime -> Sem r ()
    pushMemberLeaveEventToAll now = do
      let e = newEvent tid now (EdMemberLeave remove)
      let recipients = userRecipient (tUnqualified lusr) : membersToRecipients (Just (tUnqualified lusr)) (mems ^. teamMembers)
      when (mems ^. teamMemberListType == ListComplete) $ do
        pushNotifications
          [ def
              { origin = Just (tUnqualified lusr),
                json = toJSONObject e,
                recipients,
                transient = True
              }
          ]

removeFromConvsAndPushConvLeaveEvent ::
  forall r.
  ( Member ConversationStore r,
    Member ConversationSubsystem r
  ) =>
  Local UserId ->
  Maybe ConnId ->
  TeamId ->
  UserId ->
  Sem r ()
removeFromConvsAndPushConvLeaveEvent lusr zcon tid remove = do
  cc <- E.getTeamConversations tid
  for_ cc $ \c ->
    E.getConversation c >>= \conv ->
      for_ conv $ \dc ->
        when (remove `isMember` dc.localMembers) $
          case dc.metadata.cnvmType of
            One2OneConv ->
              E.deleteConversation dc.id_
            _ -> do
              E.deleteMembers c (UserList [remove] [])
              let (bots, allLocUsers) = localBotsAndUsers (dc.localMembers)
                  targets =
                    BotsAndMembers
                      (Set.fromList $ (.id_) <$> allLocUsers)
                      (Set.fromList $ (.id_) <$> dc.remoteMembers)
                      (Set.fromList bots)
              void $
                sendConversationActionNotifications
                  (sing @'ConversationRemoveMembersTag)
                  (tUntagged lusr)
                  True
                  zcon
                  (qualifyAs lusr dc)
                  targets
                  ( ConversationRemoveMembers
                      (pure . tUntagged . qualifyAs lusr $ remove)
                      EdReasonDeleted
                  )
                  def

getTeamConversations ::
  ( Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS OperationDenied) r,
    Member ConversationStore r,
    Member TeamSubsystem r
  ) =>
  UserId ->
  TeamId ->
  Sem r Public.TeamConversationList
getTeamConversations zusr tid = do
  tm <-
    TeamSubsystem.internalGetTeamMember zusr tid
      >>= noteS @'NotATeamMember
  unless (tm `hasPermission` GetTeamConversations) $
    throwS @OperationDenied
  Public.newTeamConversationList . fmap newTeamConversation <$> E.getTeamConversations tid

getTeamConversation ::
  ( Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS OperationDenied) r,
    Member ConversationStore r,
    Member TeamSubsystem r
  ) =>
  UserId ->
  TeamId ->
  ConvId ->
  Sem r Public.TeamConversation
getTeamConversation zusr tid cid = do
  tm <-
    TeamSubsystem.internalGetTeamMember zusr tid
      >>= noteS @'NotATeamMember
  unless (tm `hasPermission` GetTeamConversations) $
    throwS @OperationDenied
  teamConv <- E.getTeamConversation tid cid >>= noteS @'ConvNotFound
  pure $ newTeamConversation teamConv

deleteTeamConversation ::
  ( Member BackendNotificationQueueAccess r,
    Member BrigAPIAccess r,
    Member CodeStore r,
    Member ConversationStore r,
    Member (Error FederationError) r,
    Member (ErrorS 'ConvNotFound) r,
    Member (ErrorS 'InvalidOperation) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS ('ActionDenied 'Public.DeleteConversation)) r,
    Member (FederationAPIAccess FederatorClient) r,
    Member ProposalStore r,
    Member ConversationSubsystem r,
    Member TeamStore r,
    Member TeamCollaboratorsSubsystem r,
    Member E.MLSCommitLockStore r,
    Member TeamSubsystem r,
    Member (Input ConversationSubsystemConfig) r
  ) =>
  Local UserId ->
  ConnId ->
  TeamId ->
  ConvId ->
  Sem r ()
deleteTeamConversation lusr zcon _tid cid = do
  let lconv = qualifyAs lusr cid
  void $ API.deleteLocalConversation lusr zcon lconv

getSearchVisibility ::
  ( Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS OperationDenied) r,
    Member SearchVisibilityStore r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  TeamId ->
  Sem r TeamSearchVisibilityView
getSearchVisibility luid tid = do
  zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid
  void $ permissionCheck ViewTeamSearchVisibility zusrMembership
  getSearchVisibilityInternal tid

setSearchVisibility ::
  forall r.
  ( Member (ErrorS 'NotATeamMember) r,
    Member (ErrorS OperationDenied) r,
    Member (ErrorS 'TeamSearchVisibilityNotEnabled) r,
    Member SearchVisibilityStore r,
    Member TeamSubsystem r
  ) =>
  (TeamId -> Sem r Bool) ->
  Local UserId ->
  TeamId ->
  Public.TeamSearchVisibilityView ->
  Sem r ()
setSearchVisibility availableForTeam luid tid req = do
  zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid
  void $ permissionCheck ChangeTeamSearchVisibility zusrMembership
  setSearchVisibilityInternal availableForTeam tid req

-- Internal -----------------------------------------------------------------

-- | Invoke the given continuation 'k' with a list of team IDs
-- which are looked up based on:
--
-- * just limited by size
-- * an (exclusive) starting point (team ID) and size
-- * a list of team IDs
--
-- The last case returns those team IDs which have an associated
-- user. Additionally 'k' is passed in a 'hasMore' indication (which is
-- always false if the third lookup-case is used).
--
-- FUTUREWORK: avoid CPS
withTeamIds ::
  (Member TeamStore r, Member (ListItems LegacyPaging TeamId) r) =>
  UserId ->
  Maybe (Either (Range 1 32 (List TeamId)) TeamId) ->
  Range 1 100 Int32 ->
  (Bool -> [TeamId] -> Sem r a) ->
  Sem r a
withTeamIds usr range size k = case range of
  Nothing -> do
    r <- E.listItems usr Nothing (rcast size)
    k (resultSetType r == ResultSetTruncated) (resultSetResult r)
  Just (Right c) -> do
    r <- E.listItems usr (Just c) (rcast size)
    k (resultSetType r == ResultSetTruncated) (resultSetResult r)
  Just (Left (fromRange -> cc)) -> do
    ids <- E.selectTeams usr (Data.ByteString.Conversion.fromList cc)
    k False ids
{-# INLINE withTeamIds #-}

ensureUnboundUsers ::
  ( Member (ErrorS 'UserBindingExists) r,
    Member TeamStore r
  ) =>
  [UserId] ->
  Sem r ()
ensureUnboundUsers uids = do
  -- We check only 1 team because, by definition, users in binding teams
  -- can only be part of one team.
  teams <- Map.elems <$> E.getUsersTeams uids
  binds <- E.getTeamsBindings teams
  when (Binding `elem` binds) $
    throwS @'UserBindingExists

ensureNonBindingTeam ::
  ( Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'NoAddToBinding) r,
    Member TeamStore r
  ) =>
  TeamId ->
  Sem r ()
ensureNonBindingTeam tid = do
  team <- noteS @'TeamNotFound =<< E.getTeam tid
  when (tdTeam team ^. teamBinding == Binding) $
    throwS @'NoAddToBinding

-- ensure that the permissions are not "greater" than the user's copy permissions
-- this is used to ensure users cannot "elevate" permissions
ensureNotElevated :: (Member (ErrorS 'InvalidPermissions) r) => Permissions -> TeamMember -> Sem r ()
ensureNotElevated targetPermissions member =
  unless
    ( targetPermissions.self
        `Set.isSubsetOf` (member ^. permissions).copy
    )
    $ throwS @'InvalidPermissions

ensureNotTooLarge ::
  ( Member BrigAPIAccess r,
    Member (ErrorS 'TooManyTeamMembers) r,
    Member (Input Opts) r
  ) =>
  TeamId ->
  Sem r TeamSize
ensureNotTooLarge tid = do
  o <- input
  (TeamSize size) <- E.getSize tid
  unless (size < fromIntegral (o ^. settings . maxTeamSize)) $
    throwS @'TooManyTeamMembers
  pure $ TeamSize size

-- | Ensure that a team doesn't exceed the member count limit for the LegalHold
-- feature. A team with more members than the fanout limit is too large, because
-- the fanout limit would prevent turning LegalHold feature _off_ again (for
-- details see 'Galley.API.LegalHold.removeSettings').
--
-- If LegalHold is configured for whitelisted teams only we consider the team
-- size unlimited, because we make the assumption that these teams won't turn
-- LegalHold off after activation.
--  FUTUREWORK: Find a way around the fanout limit.
ensureNotTooLargeForLegalHold ::
  forall r.
  ( Member LegalHoldStore r,
    Member TeamFeatureStore r,
    Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r,
    Member (Input FanoutLimit) r,
    Member (Input (FeatureDefaults LegalholdConfig)) r
  ) =>
  TeamId ->
  Int ->
  Sem r ()
ensureNotTooLargeForLegalHold tid teamSize =
  whenM (isLegalHoldEnabledForTeam tid) $
    unlessM (teamSizeBelowLimit teamSize) $
      throwS @'TooManyTeamMembersOnTeamWithLegalhold

addTeamMemberInternal ::
  ( Member BrigAPIAccess r,
    Member (ErrorS 'TooManyTeamMembers) r,
    Member (ErrorS 'TooManyTeamAdmins) r,
    Member NotificationSubsystem r,
    Member (Input Opts) r,
    Member Now r,
    Member TeamNotificationStore r,
    Member TeamStore r,
    Member P.TinyLog r
  ) =>
  TeamId ->
  Maybe UserId ->
  Maybe ConnId ->
  NewTeamMember ->
  Sem r TeamSize
addTeamMemberInternal tid origin originConn (ntmNewTeamMember -> new) = do
  P.debug $
    Log.field "targets" (toByteString (new ^. userId))
      . Log.field "action" (Log.val "Teams.addTeamMemberInternal")
  sizeBeforeAdd <- ensureNotTooLarge tid

  admins <- E.getTeamAdmins tid
  let admins' = [new ^. userId | isAdminOrOwner (new ^. M.permissions)] <> admins
  checkAdminLimit (length admins')

  E.createTeamMember tid new

  now <- Now.get
  let e = newEvent tid now (EdMemberJoin (new ^. userId))
  let recipients = case origin of
        Just o -> userRecipient <$> o : filter (/= o) ((new ^. userId) : admins')
        Nothing -> userRecipient <$> new ^. userId : admins'
  pushNotifications
    [ def
        { origin = Just (new ^. userId),
          json = toJSONObject e,
          recipients,
          conn = originConn,
          transient = True
        }
    ]

  APITeamQueue.pushTeamEvent tid e
  pure sizeBeforeAdd

getBindingTeamMembers ::
  ( Member (ErrorS 'TeamNotFound) r,
    Member (ErrorS 'NonBindingTeam) r,
    Member TeamStore r,
    Member (Input FanoutLimit) r,
    Member TeamSubsystem r
  ) =>
  UserId ->
  Sem r TeamMemberList
getBindingTeamMembers zusr = do
  tid <- E.lookupBindingTeam zusr
  getTeamMembersForFanout tid

-- This could be extended for more checks, for now we test only legalhold
--
-- Brig's `POST /register` endpoint throws the errors returned by this endpoint
-- verbatim.
--
-- FUTUREWORK: When this enpoint gets Servantified, it should have a more
-- precise list of errors, LegalHoldError is too wide, currently this can
-- actaully only error with TooManyTeamMembersOnTeamWithLegalhold. Once we have
-- a more precise list of errors and the endpoint is servantified, we can use
-- those to enrich 'Wire.API.User.RegisterError' and ensure that these errors
-- also show up in swagger. Currently, the error returned by this endpoint is
-- thrown in IO, we could then refactor that to be thrown in `ExceptT
-- RegisterError`.
canUserJoinTeam ::
  forall r.
  ( Member BrigAPIAccess r,
    Member LegalHoldStore r,
    Member TeamFeatureStore r,
    Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r,
    Member (Input FanoutLimit) r,
    Member (Input (FeatureDefaults LegalholdConfig)) r
  ) =>
  TeamId ->
  Sem r ()
canUserJoinTeam tid = do
  lhEnabled <- isLegalHoldEnabledForTeam tid
  when lhEnabled $ do
    (TeamSize sizeBeforeJoin) <- E.getSize tid
    ensureNotTooLargeForLegalHold tid (fromIntegral sizeBeforeJoin + 1)

-- | Modify and get visibility type for a team (internal, no user permission checks)
getSearchVisibilityInternal ::
  (Member SearchVisibilityStore r) =>
  TeamId ->
  Sem r TeamSearchVisibilityView
getSearchVisibilityInternal =
  fmap TeamSearchVisibilityView
    . SearchVisibilityData.getSearchVisibility

setSearchVisibilityInternal ::
  forall r.
  ( Member (ErrorS 'TeamSearchVisibilityNotEnabled) r,
    Member SearchVisibilityStore r
  ) =>
  (TeamId -> Sem r Bool) ->
  TeamId ->
  TeamSearchVisibilityView ->
  Sem r ()
setSearchVisibilityInternal availableForTeam tid (TeamSearchVisibilityView searchVisibility) = do
  unlessM (availableForTeam tid) $
    throwS @'TeamSearchVisibilityNotEnabled
  SearchVisibilityData.setSearchVisibility tid searchVisibility

userIsTeamOwner ::
  ( Member (ErrorS 'TeamMemberNotFound) r,
    Member (ErrorS 'AccessDenied) r,
    Member (ErrorS 'NotATeamMember) r,
    Member (Input (Local ())) r,
    Member TeamSubsystem r
  ) =>
  TeamId ->
  UserId ->
  Sem r ()
userIsTeamOwner tid uid = do
  asking <- qualifyLocal uid
  mem <- getTeamMember asking tid uid
  unless (isTeamOwner mem) $ throwS @'AccessDenied

-- Queues a team for async deletion
queueTeamDeletion ::
  ( Member (ErrorS 'DeleteQueueFull) r,
    Member (Queue DeleteItem) r
  ) =>
  TeamId ->
  UserId ->
  Maybe ConnId ->
  Sem r ()
queueTeamDeletion tid zusr zcon = do
  ok <- E.tryPush (TeamItem tid zusr zcon)
  unless ok $ throwS @'DeleteQueueFull

checkAdminLimit :: (Member (ErrorS 'TooManyTeamAdmins) r) => Int -> Sem r ()
checkAdminLimit adminCount =
  when (adminCount > 2000) $
    throwS @'TooManyTeamAdmins

-- | Updating a team collaborator permissions eventually cleaning their conversations
updateTeamCollaborator ::
  forall r.
  ( Member ConversationStore r,
    Member (ErrorS OperationDenied) r,
    Member (ErrorS NotATeamMember) r,
    Member P.TinyLog r,
    Member TeamCollaboratorsSubsystem r,
    Member ConversationSubsystem r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  TeamId ->
  UserId ->
  Set TeamCollaborator.CollaboratorPermission ->
  Sem r ()
updateTeamCollaborator lusr tid rusr perms = do
  P.debug $
    Log.field "targets" (toByteString rusr)
      . Log.field "action" (Log.val "Teams.updateTeamCollaborator")
  zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid
  void $ permissionCheck UpdateTeamCollaborator zusrMember
  when (Set.null $ Set.intersection (Set.fromList [Collaborator.CreateTeamConversation, Collaborator.ImplicitConnection]) perms) $
    removeFromConvsAndPushConvLeaveEvent lusr Nothing tid rusr
  internalUpdateTeamCollaborator rusr tid perms

-- | Removing a team collaborator and clean their conversations
removeTeamCollaborator ::
  forall r.
  ( Member ConversationStore r,
    Member (ErrorS OperationDenied) r,
    Member (ErrorS NotATeamMember) r,
    Member NotificationSubsystem r,
    Member ConversationSubsystem r,
    Member (Input Opts) r,
    Member Now r,
    Member P.TinyLog r,
    Member TeamFeatureStore r,
    Member TeamStore r,
    Member TeamCollaboratorsSubsystem r,
    Member (Input FanoutLimit) r,
    Member TeamSubsystem r
  ) =>
  Local UserId ->
  TeamId ->
  UserId ->
  Sem r ()
removeTeamCollaborator lusr tid rusr = do
  P.debug $
    Log.field "targets" (toByteString rusr)
      . Log.field "action" (Log.val "Teams.removeTeamCollaborator")
  zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid
  void $ permissionCheck RemoveTeamCollaborator zusrMember
  toNotify <-
    getFeatureForTeam @LimitedEventFanoutConfig tid
      >>= ( \case
              FeatureStatusEnabled -> Left <$> E.getTeamAdmins tid
              FeatureStatusDisabled -> Right <$> getTeamMembersForFanout tid
          )
        . (.status)
  uncheckedDeleteTeamMember lusr Nothing tid rusr toNotify
  internalRemoveTeamCollaborator rusr tid
  now <- Now.get
  let e = newEvent tid now (EdCollaboratorRemove rusr)
  admins <- E.getTeamAdmins tid
  pushNotifications
    [ def
        { origin = Just $ tUnqualified lusr,
          json = toJSONObject e,
          recipients = userRecipient rusr : map (`Recipient` RecipientClientsAll) admins
        }
    ]
